﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Xml;

using BizElements.BusinessLayer;

namespace BizElements.Web
{
    /// <summary>
    /// Defines how <see cref="ICommand"/> objects are grouped and displayed in UI.
    /// </summary>
    /// <remarks>Command hierarchy and design layout may be stored in an XML file.</remarks>
    public sealed class CommandGroupStyle
    {
        #region FieldPermissions.

        string key;
        string text;
        string description;
        IList<CommandGroupItem> items = new List<CommandGroupItem>();
        ObjectProperties extendedProperties = new ObjectProperties();

        #endregion

        #region Constructors.

        /// <summary>
        /// Initializes a new instance of <b>CommandGroupStyle</b> class.
        /// </summary>
        public CommandGroupStyle()
        { }

        /// <summary>
        /// Initializes a new instance of <b>CommandGroupStyle</b> class.
        /// </summary>
        /// <param name="key">Optional group key. Nullable. Empty strings are converted to <b>null</b>.</param>
        /// <param name="text">Optional text. Nullable. Empty strings are converted to <b>null</b>.</param>
        /// <param name="description">Optional description. Nullable. Empty strings are converted to <b>null</b>.</param>
        public CommandGroupStyle(string key, string text, string description)
        {
            this.Key = key;
            this.Text = text;
            this.Description = description;
        }

        /// <summary>
        /// Initializes a new instance of <b>CommandGroupStyle</b> class.
        /// </summary>
        /// <param name="commands">A list of ICommand objects from which <see cref="CommandStyle"/> items
        /// are created and added to the group.</param>
        public CommandGroupStyle(IList<ICommand> commands)
        {
            foreach (ICommand cmd in commands)
                this.Items.Add(new CommandGroupItem(new CommandStyle(cmd)));
        }

        /// <summary>
        /// Initializes a new instance of <b>CommandGroupStyle</b> class.
        /// </summary>
        /// <param name="commands">A list of ICommand objects from which <see cref="CommandStyle"/> items
        /// are created and added to the group.</param>
        /// <param name="minParameterSetSize">Only commands whose <see cref="ICommand.MinParameterSetSize"/> propery
        /// is greater or equal to the provided value are added to the group. Negative values are ignored.</param>
        /// <param name="maxParameterSetSize">Only commands whose <see cref="ICommand.MaxParameterSetSize"/> propery
        /// is less or equal to the provided value are added to the group. Negative values are ignored.</param>
        public CommandGroupStyle(IList<ICommand> commands, int minParameterSetSize, int maxParameterSetSize)
        {
            foreach (ICommand cmd in commands)
            {
                bool obeysMinSize = (minParameterSetSize >= 0) ? cmd.MinParameterSetSize >= minParameterSetSize : true;
                bool obeysMaxSize = (maxParameterSetSize >= 0) ? cmd.MaxParameterSetSize <= maxParameterSetSize : true;
                if (obeysMinSize && obeysMaxSize)
                    this.Items.Add(new CommandGroupItem(new CommandStyle(cmd)));
            }
        }

        #endregion

        #region FindCommand.

        /// <summary>
        /// Recursively searches the collection for the command with the specified <see cref="CommandStyle.Key"/> value.
        /// </summary>
        /// <param name="key">Specifies the command to look for.</param>
        /// <returns>CommandStyle that matches the specified key; <b>null</b> if not found.</returns>
        public CommandStyle FindCommand(string key)
        {
            foreach (CommandGroupItem item in this.items)
            {
                if (item.Command != null)
                {
                    CommandStyle candidate = item.Command;
                    bool isMatch = (candidate != null) && (candidate.Key == key);
                    if (isMatch)
                        return candidate;
                }
                else
                {
                    CommandStyle foundInChildren = item.Group.FindCommand(key);
                    if (foundInChildren != null)
                        return foundInChildren;
                }
            }

            return null;
        }

        #endregion

        #region FromXml.

        private static string GetFileCacheKey(string fileName)
        {
            return "CommandGroupStyle_E658C1D1-DF61-4de2-A713-54FB42813A91_" + fileName;
        }

        private static string GetFileCacheKey(string fileName, CultureInfo culture)
        {
            if (culture == null)
                return GetFileCacheKey(fileName);
            else
                return "CommandGroupStyle_E658C1D1-DF61-4de2-A713-54FB42813A91_" + fileName + "_" + culture.Name;
        }

        /// <summary>
        /// Reads a command hierarchy and style data from an XML file.
        /// </summary>
        /// <param name="fileName">The filename (including the path) from which to read.</param>
        /// <remarks>The method automatically caches file contents in <see cref="GlobalCache"/> to improve performance by reducing the number of IO requests.</remarks>
        public void ReadXmlFile(string fileName)
        {
            string cacheKey = GetFileCacheKey(fileName);
            string xmlContent = GlobalCache.GetData(cacheKey) as string;
            if (xmlContent == null)
            {
                XmlDocument xml = LoadXmlDocument(fileName);
                GlobalCache.SetData(cacheKey, xml.OuterXml);
                FromXmlElement(xml.DocumentElement);
            }
            else
            {
                ReadXmlContent(xmlContent);
            }
        }

        /// <summary>
        /// Reads a command hierarchy and style data from a localized XML file.
        /// </summary>
        /// <param name="fileName">The filename (including the path) from which to read data for the default/invariant culture.</param>
        /// <param name="culture">Specifies culture and language of the requested file. Nullable.</param>
        /// <remarks>The method tries to find the closest possible match for the specified culture. File naming convention 
        /// follows the convention for RESX files. Examples of valid file names:
        /// <list type="bullet">
        /// <item>c:\MyFolder\MyCommands.xml</item>
        /// <item>c:\MyFolder\MyCommands.en.xml</item>
        /// <item>c:\MyFolder\MyCommands.en-US.xml</item>
        /// </list>
        /// </remarks>
        public void ReadXmlFile(string fileName, CultureInfo culture)
        {
            if (culture == null)
            {
                ReadXmlFile(fileName);
                return;
            }

            string cacheKey = GetFileCacheKey(fileName, culture);
            string xmlContent = GlobalCache.GetData(cacheKey) as string;
            if (xmlContent == null)
            {
                string woExtension, extension;
                GetFilePathParts(fileName, out woExtension, out extension);
                string[] cultureNameParts = GetCultureNameParts(culture);
                string localizedFilePath = FindClosestCulturePath(woExtension, extension, cultureNameParts);

                XmlDocument xml = LoadXmlDocument(localizedFilePath);
                GlobalCache.SetData(cacheKey, xml.OuterXml);
                FromXmlElement(xml.DocumentElement);
            }
            else
            {
                ReadXmlContent(xmlContent);
            }
        }

        private static XmlDocument LoadXmlDocument(string filePath)
        {
            XmlDocument xml = new XmlDocument();
            using (StreamReader file = new StreamReader(filePath))
            {
                xml.Load(file);
            }

            return xml;
        }

        private static string[] GetCultureNameParts(CultureInfo culture)
        {
            return culture.Name.Split('-');
        }

        private static void GetFilePathParts(string fileName, out string woExtension, out string extension)
        {
            int dotIndex = fileName.LastIndexOf('.');
            bool containsDot = (dotIndex >= 0);
            woExtension = (containsDot) ? fileName.Remove(dotIndex) : fileName;
            bool hasExtension = (containsDot) && (fileName.Length > dotIndex + 1);
            extension = (hasExtension) ? fileName.Substring(dotIndex + 1) : "";
        }

        private static string FindClosestCulturePath(string woExtension, string extension, string[] cultureNameParts)
        {
            // Go from most specific to most generic culture.
            for (int localizationDegree = cultureNameParts.Length; localizationDegree >= 0; localizationDegree--)
            {
                string localizedPath = GetLocalizedPath(woExtension, extension, cultureNameParts, localizationDegree);
                if (File.Exists(localizedPath))
                    return localizedPath;
            }

            return woExtension + "." + extension;
        }

        private static string GetLocalizedPath(string woExtension, string extension, string[] cultureNameParts, int localizationDegree)
        {
            string cultureName = GetCultureName(cultureNameParts, localizationDegree - 1);
            if (string.IsNullOrEmpty(cultureName))
                return woExtension + "." + extension;
            else
                return woExtension + "." + cultureName + "." + extension;
        }

        private static string GetCultureName(string[] cultureNameParts, int endIndex)
        {
            string name = "";
            for (int idx = 0; idx <= endIndex; idx++)
            {
                name += cultureNameParts[idx];
                bool isLastPart = (idx == endIndex);
                if (!isLastPart)
                    name += "-";
            }

            return name;
        }

        /// <summary>
        /// Reads a command hierarchy and style data from an XML document. 
        /// </summary>
        /// <param name="xml">XML document.</param>
        public void ReadXmlDocument(XmlDocument xml)
        {
            FromXmlElement(xml.DocumentElement);
        }

        /// <summary>
        /// Reads a command hierarchy and style data from an XML document.
        /// </summary>
        /// <param name="xmlContent">XML document content.</param>
        public void ReadXmlContent(string xmlContent)
        {
            XmlDocument xml = new XmlDocument();
            xml.Load(new StringReader(xmlContent));
            FromXmlElement(xml.DocumentElement);
        }

        private void FromXmlElement(XmlElement xmlCommandGroupElement)
        {
            this.Items.Clear();
            EnsureCorrectElement(xmlCommandGroupElement, XML.CommandGroup);
            foreach (XmlAttribute attr in xmlCommandGroupElement.Attributes)
                ParseCommandGroupAttribute(attr);

            foreach (XmlNode child in xmlCommandGroupElement.ChildNodes)
            {
                if (child.NodeType == XmlNodeType.Element)
                    ParseCommandGroupChildElement((XmlElement)child);
            }
        }

        private static class XML
        {
            public const string CommandGroup = "CommandGroup";
            public const string Key = "Key";
            public const string Text = "Text";
            public const string Command = "Command";
            public const string Description = "Description";
            public const string ExtendedProperties = "ExtendedProperties";
            public const string Property = "Property";
            public const string Indexer = "Indexer";
            public const string Value = "Value";
            public const string Index = "Index";
        }

        private static void EnsureCorrectElement(XmlElement element, string expectedElementName)
        {
            if (element.LocalName != expectedElementName)
                throw new ArgumentException("Expected '" + expectedElementName + "' XML element but '" + element.LocalName + "'was provided.");
        }

        private void ParseCommandGroupAttribute(XmlAttribute attr)
        {
            switch (attr.LocalName)
            {
                case XML.Key:
                    this.Key = attr.Value.Trim();
                    break;

                case XML.Text:
                    this.Text = attr.Value.Trim();
                    break;

                case XML.Description:
                    this.Description = attr.Value.Trim();
                    break;

                default:
                    // Ignore unsupported attributes.
                    break;
            }
        }

        private void ParseCommandGroupChildElement(XmlElement child)
        {
            switch (child.LocalName)
            {
                case XML.ExtendedProperties:
                    ParseExtendedPropertiesElement(child);
                    break;

                case XML.CommandGroup:
                    CommandGroupStyle subGroup = new CommandGroupStyle();
                    subGroup.FromXmlElement(child);
                    this.Items.Add(new CommandGroupItem(subGroup));
                    break;

                case XML.Command:
                    CommandStyle cmd = new CommandStyle();
                    cmd.FromXmlElement(child);
                    this.Items.Add(new CommandGroupItem(cmd));
                    break;

                default:
                    // Ignore unsupported elements.
                    break;
            }
        }

        private void ParseExtendedPropertiesElement(XmlElement xmlExtendedProperties)
        {
            EnsureCorrectElement(xmlExtendedProperties, XML.ExtendedProperties);
            foreach (XmlNode propertyOrIndexer in xmlExtendedProperties.ChildNodes)
            {
                if (propertyOrIndexer.NodeType != XmlNodeType.Element)
                    continue;

                switch (propertyOrIndexer.LocalName)
                {
                    case XML.Property:
                        ParsePropertyElement((XmlElement)propertyOrIndexer);
                        break;

                    case XML.Indexer:
                        ParseIndexerElement((XmlElement)propertyOrIndexer);
                        break;

                    default:
                        // Ignore unsupported elements.
                        break;
                }
            }
        }

        private void ParsePropertyElement(XmlElement xmlProperty)
        {
            EnsureCorrectElement(xmlProperty, XML.Property);
            string property = null;
            string value = null;
            foreach (XmlAttribute attr in xmlProperty.Attributes)
            {
                switch (attr.LocalName)
                {
                    case XML.Property:
                        property = attr.Value.Trim();
                        break;

                    case XML.Value:
                        value = attr.Value.Trim();
                        break;

                    default:
                        // Ignore unsupported attributes.
                        break;
                }
            }

            if (string.IsNullOrEmpty(property) || string.IsNullOrEmpty(value))
                throw new ArgumentException("xmlProperty", "The '" + XML.Property + "' element must contain non-empty '" + XML.Property + "' and '" + XML.Value + "' attributes.");

            this.extendedProperties.Properties.Add(property, value);
        }

        private void ParseIndexerElement(XmlElement xmlIndexer)
        {
            EnsureCorrectElement(xmlIndexer, XML.Indexer);
            string property = null;
            string index = null;
            string value = null;
            foreach (XmlAttribute attr in xmlIndexer.Attributes)
            {
                switch (attr.LocalName)
                {
                    case XML.Property:
                        property = attr.Value.Trim();
                        break;

                    case XML.Index:
                        index = attr.Value.Trim();
                        break;

                    case XML.Value:
                        value = attr.Value.Trim();
                        break;

                    default:
                        // Ignore unsupported attributes.
                        break;
                }
            }

            if (string.IsNullOrEmpty(property) || string.IsNullOrEmpty(index) || string.IsNullOrEmpty(value))
                throw new ArgumentException("xmlIndexer", "The '" + XML.Indexer + "' element must contain non-empty '" + XML.Property + "', '" + XML.Index + "' and '" + XML.Value + "' attributes.");

            this.extendedProperties.Indexers[property, index] = value;
        }

        #endregion

        #region Properties.

        /// <summary>
        /// Gets or sets optional group key. Nullable. Empty strings are converted to <b>null</b>.
        /// </summary>
        public string Key
        {
            get { return this.key; }
            set { this.key = string.IsNullOrEmpty(value) ? null : value; }
        }

        /// <summary>
        /// Gets or sets optional text. Nullable. Empty strings are converted to <b>null</b>.
        /// </summary>
        public string Text
        {
            get { return this.text; }
            set { this.text = string.IsNullOrEmpty(value) ? null : value; }
        }

        /// <summary>
        /// Gets or sets optional description. Nullable. Empty strings are converted to <b>null</b>.
        /// </summary>
        public string Description
        {
            get { return this.description; }
            set { this.description = string.IsNullOrEmpty(value) ? null : value; }
        }

        /// <summary>
        /// Gets a collection of items contained in the current group.
        /// </summary>
        public IList<CommandGroupItem> Items
        {
            get { return this.items; }
        }

        /// <summary>
        /// Gets a collection of additional properties which affect how commands are displayed in UI.
        /// </summary>
        /// <remarks>For example, ExtendedProperties may specify a CssClass property for a Web control.</remarks>
        public ObjectProperties ExtendedProperties
        {
            get { return this.extendedProperties; }
        }

        #endregion
    }
}